/*-------------------------------------------------------------------------
 *
 * passwordcheck.c
 *
 *
 * Copyright (c) 2009-2022, PostgreSQL Global Development Group
 *
 * 作者： Laurenz Albe <laurenz.albe@wien.gv.at>
 *
 * IDENTIFICATION
 *	  contrib/passwordcheck/passwordcheck.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include <ctype.h>

#ifdef USE_CRACKLIB
#include <crack.h>
#endif

#include "commands/user.h"
#include "fmgr.h"
#include "libpq/crypt.h"

PG_MODULE_MAGIC;

/* 保存钩子值以备卸载时使用 */
static check_password_hook_type prev_check_password_hook = NULL;

/* 密码长度短于此将被拒绝 */
#define MIN_PWD_LENGTH 8

extern void _PG_init(void);

/*
 * check_password
 *
 * 对加密或未加密的密码进行检查
 * 如果不可接受则报告错误
 *
 * username: 正在创建或更改的角色名称
 * password: 新密码（可能已经加密）
 * password_type: PASSWORD_TYPE_* 代码，用于指示密码是
 *			以明文还是加密形式。
 * validuntil_time: 密码到期时间，作为一个 timestamptz 数据
 * validuntil_null: 如果密码到期时间为 NULL，则为 true
 *
 * 此示例实现未关注密码
 * 到期时间，但您可能希望坚持要求它为非空且
 * 不要太远在未来。
 */
static void fc_check_password(const char *fc_username,
			   const char *fc_shadow_pass,
			   PasswordType fc_password_type,
			   Datum fc_validuntil_time,
			   bool fc_validuntil_null)
{
	if (prev_check_password_hook)
		prev_check_password_hook(fc_username, fc_shadow_pass,
								 fc_password_type, fc_validuntil_time,
								 fc_validuntil_null);

	if (fc_password_type != PASSWORD_TYPE_PLAINTEXT)
	{
		/*
		 * 不幸的是，我们无法对加密
		 * 密码进行全面检查 - 我们受到猜测的限制。 （另外，我们可以
		 * 坚持要求密码以非加密形式呈现，但这有
		 * 自身的安全缺点。）
		 *
		 * 我们仅检查 username = password。
		 */
		const char *fc_logdetail = NULL;

		if (plain_crypt_verify(fc_username, fc_shadow_pass, fc_username, &fc_logdetail) == STATUS_OK)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("password must not equal user name")));
	}
	else
	{
		/*
		 * 对于未加密的密码，我们可以进行更好的检查
		 */
		const char *fc_password = fc_shadow_pass;
		int			fc_pwdlen = strlen(fc_password);
		int			fc_i;
		bool		fc_pwd_has_letter,
					fc_pwd_has_nonletter;
#ifdef USE_CRACKLIB
		const char *reason;
#endif

		/* 强制最低长度 */
		if (fc_pwdlen < MIN_PWD_LENGTH)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("password is too short")));

		/* 检查密码是否包含用户名 */
		if (strstr(fc_password, fc_username))
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("password must not contain user name")));

		/* 检查密码是否同时包含字母和非字母 */
		fc_pwd_has_letter = false;
		fc_pwd_has_nonletter = false;
		for (fc_i = 0; fc_i < fc_pwdlen; fc_i++)
		{
			/*
			 * isalpha() 对多字节编码无效，但我们
			 * 将非 ASCII 字符视为非字母
			 */
			if (isalpha((unsigned char) fc_password[fc_i]))
				fc_pwd_has_letter = true;
			else
				fc_pwd_has_nonletter = true;
		}
		if (!fc_pwd_has_letter || !fc_pwd_has_nonletter)
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("password must contain both letters and nonletters")));

#ifdef USE_CRACKLIB
		/* 调用 cracklib 检查密码 */
		if ((reason = FascistCheck(password, CRACKLIB_DICTPATH)))
			ereport(ERROR,
					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
					 errmsg("password is easily cracked"),
					 errdetail_log("cracklib diagnostic: %s", reason)));
#endif
	}

	/* 所有检查通过，密码是可以的 */
}

/*
 * 模块初始化函数
 */
void _PG_init(void)
{
	/* 在模块加载时激活密码检查 */
	prev_check_password_hook = check_password_hook;
	check_password_hook = fc_check_password;
}
